package br.com.estevam.sisfin.daos;

import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Date;

import org.hibernate.EntityMode;
import org.hibernate.HibernateException;
import org.hibernate.StatelessSession;
import org.hibernate.cfg.Configuration;
import org.hibernate.event.Initializable;
import org.hibernate.event.PreDeleteEvent;
import org.hibernate.event.PreDeleteEventListener;
import org.hibernate.event.PreInsertEvent;
import org.hibernate.event.PreInsertEventListener;
import org.hibernate.event.PreLoadEvent;
import org.hibernate.event.PreLoadEventListener;
import org.hibernate.event.PreUpdateEvent;
import org.hibernate.event.PreUpdateEventListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import br.com.estevam.sisfin.entities.AuditTrail;

public final class HibernateAuditLogListener implements 
PreDeleteEventListener, PreInsertEventListener, PreUpdateEventListener,
PreLoadEventListener, Initializable {

	private static final long serialVersionUID = 1L;
	private static final Logger LOG = LoggerFactory.getLogger(HibernateAuditLogListener.class);
	private static final String OPERATION_TYPE_INSERT = "INSERT";
	private static final String OPERATION_TYPE_UPDATE = "UPDATE";
	private static final String OPERATION_TYPE_DELETE = "DELETE";

	public final void initialize(final Configuration cfg) {
		//
	}

	public final boolean onPreDelete(final PreDeleteEvent event) {
		try {

			final Long actorId = idUser(event.getEntity().toString());
			final Serializable entityId = event.getPersister().hasIdentifierProperty() ? event.getPersister().getIdentifier(event.getEntity(), event.getPersister().guessEntityMode(event.getEntity())) : null;
			final String entityName = tirarPontoEntity(event.getEntity().getClass().toString());
			final EntityMode entityMode = event.getPersister().guessEntityMode(event.getEntity());
			final Date transTime = new Date();
			Object newPropValue = null;

			// need to have a separate session for audit save
			StatelessSession session = event.getPersister().getFactory().openStatelessSession();
			session.beginTransaction();

			if (LOG.isDebugEnabled()) {
				LOG.debug("{} for: {}, ID: {}, actor: {}, date: {}", new Object[] { entityName, entityId, actorId, transTime });
			}

			for (String propertyName : event.getPersister().getPropertyNames()) {
				newPropValue = event.getPersister().getPropertyValue(event.getEntity(), propertyName, entityMode);
				// because we are performing an insert we only need to be concerned will non-null values
				if (newPropValue != null) {
					// collections will fire their own events
					if (!(newPropValue instanceof Collection<?>)) {
						if (LOG.isDebugEnabled()) {
							LOG.debug("{} for: {}, ID: {}, property: {}, value: {}, actor: {}, date: {}", new Object[] { OPERATION_TYPE_DELETE, entityName, entityId, propertyName, newPropValue, actorId, transTime });
						}
						session.insert(new AuditTrail(
								entityId.toString(), 
								entityName, 
								propertyName, 
								newPropValue != null ? propertiesEntity(newPropValue.toString()) : null, 
										null, 
										OPERATION_TYPE_DELETE, 
										actorId, 
										transTime));
					}
				}
			}

			session.getTransaction().commit();
			session.close();
		} catch (HibernateException e) {
			LOG.error("Unable to process audit log for DELETE operation", e);
		}
		return false;
	}

	public final boolean onPreInsert(final PreInsertEvent event) {
		try {

			final Long actorId = idUser(event.getEntity().toString());
//			final String entityId = event.getPersister().hasIdentifierProperty() ? event.getPersister().getIdentifier(event.getEntity(), event.getPersister().guessEntityMode(event.getEntity())).toString() : "";
			final Serializable entityId = event.getPersister().hasIdentifierProperty() ? event.getPersister().getIdentifier(event.getEntity(), event.getPersister().guessEntityMode(event.getEntity()))	: null;
			final String entityName = tirarPontoEntity(event.getEntity().getClass().toString());
			final EntityMode entityMode = event.getPersister().guessEntityMode(event.getEntity());
			final Date transTime = new Date();
			Object newPropValue = null;

			// need to have a separate session for audit save
			StatelessSession session = event.getPersister().getFactory().openStatelessSession();
			session.beginTransaction();

			for (String propertyName : event.getPersister().getPropertyNames()) {
				newPropValue = event.getPersister().getPropertyValue(event.getEntity(), propertyName, entityMode);
				// because we are performing an insert we only need to be concerned will non-null values
				if (newPropValue != null) {
					// collections will fire their own events
					if (!(newPropValue instanceof Collection<?>)) {
						if (LOG.isDebugEnabled()) {
							LOG.debug("{} for: {}, ID: {}, property: {}, value: {}, actor: {}, date: {}", new Object[] { OPERATION_TYPE_INSERT, entityName, entityId, propertyName, newPropValue, actorId, transTime });
						}

						if(newPropValue instanceof Date){
							newPropValue = formatDate(newPropValue);
						}

						session.insert(new AuditTrail(
								entityId.toString(), 
								entityName, 
								propertyName, 
								null, 
								newPropValue != null ? propertiesEntity(newPropValue.toString()) : null, 
										OPERATION_TYPE_INSERT, 
										actorId, 
										transTime));
					}
				}
			}

			session.getTransaction().commit();
			session.close();
		} catch (HibernateException e) {
			LOG.error("Unable to process audit log for INSERT operation", e);
		}
		return false;
	}

	public final boolean onPreUpdate(PreUpdateEvent event) {
		try {

			final Long actorId = idUser(event.getEntity().toString());
			final Serializable entityId = event.getPersister().hasIdentifierProperty() ? event.getPersister().getIdentifier(event.getEntity(), event.getPersister().guessEntityMode(event.getEntity()))	: null;
			final String entityName = tirarPontoEntity(event.getEntity().getClass().toString());
			final EntityMode entityMode = event.getPersister().guessEntityMode(event.getEntity());
			final Date transTime = new Date();
			Object oldPropValue = null;
			Object newPropValue = null;

			// need to have a separate session for audit save
			StatelessSession session = event.getPersister().getFactory().openStatelessSession();
			session.beginTransaction();

			// get the existing entity from session so that we can extract existing property values
			Object existingEntity = session.get(event.getEntity().getClass(), entityId);

			// cycle through property names, extract corresponding property values and insert new entry in audit trail
			for (String propertyName : event.getPersister().getPropertyNames()) {
				newPropValue = event.getPersister().getPropertyValue(event.getEntity(), propertyName, entityMode);
				// because we are performing an insert we only need to be concerned will non-null values
				if (newPropValue != null) {
					// collections will fire their own events
					if (!(newPropValue instanceof Collection<?>)) {
						oldPropValue = event.getPersister().getPropertyValue(existingEntity, propertyName, entityMode);
						if (LOG.isDebugEnabled()) {
							LOG.debug("{} for: {}, ID: {}, property: {}, old value: {}, new value: {}, actor: {}, date: {}", new Object[] { OPERATION_TYPE_UPDATE, entityName, entityId, propertyName, oldPropValue, newPropValue, actorId, transTime });
						}

						if(newPropValue instanceof Date){
							newPropValue = formatDate(newPropValue);
						}

						session.insert(new AuditTrail(
								entityId.toString(), 
								entityName, 
								propertyName, 
								oldPropValue != null ? propertiesEntity(oldPropValue.toString()) : null, 
										newPropValue != null ? propertiesEntity(newPropValue.toString()) : null, 
												OPERATION_TYPE_UPDATE, 
												actorId, 
												transTime
						));
					}
				}
			}

			session.getTransaction().commit();
			session.close();
		} catch (HibernateException e) {
			LOG.error("Unable to process audit log for UPDATE operation", e);
		}
		return false;
	}

	public final void onPreLoad(final PreLoadEvent event) {
		// TODO : complete load audit log if desired
	}

	private final String tirarPontoEntity(String entityAntes){
		String entityNova = entityAntes;
		while(true){
			if(entityNova.indexOf(".") > 0){
				entityNova = entityNova.substring(entityNova.indexOf(".")+1);
			} else {
				break;
			}
		}
		return entityNova;
	}

	private final Object formatDate(Object object) {
		Object o = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(object);

		if(o.toString().contains("00:00:00.000")){
			o = o.toString().substring(0, 10);
		}

		return o;
	}

	private final Long idUser(String eventIdUser) {
		return Long.parseLong(eventIdUser.split("---")[1]);
	}

	private final String propertiesEntity(String eventPropertiesEntity) {
		if(eventPropertiesEntity.split("---").length > 0){
			return eventPropertiesEntity.split("---")[0];
		} else {
			return eventPropertiesEntity;
		}
	}

}
